package scenic;

import java.awt.Font;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;

import scenic.geom.Affine2;
import scenic.jni.RenderCanvas;
import scenic.jni.Renderer;

/**
 * This component is used to display scene graphs. Users of
 * the Scenic library should extend this class and override the
 * updateScene() method to create graphics. All changes to the scene
 * graph should be done in the updateScene() method.
 * Never call updateScene() directly. Instead, you should call the invalidateScene()
 * method.
 */
public class ScenicCanvas extends RenderCanvas implements SceneParent {
	private static final long serialVersionUID = 1L;
	
	private class ResizeListener extends ComponentAdapter {
		public void componentResized(ComponentEvent e) {
			invalidateScene();
		}
	}
	
	SceneNode scene;
	Color bgColor = new Color(0.0f, 0.0f, 0.0f);
	boolean sceneChanged;
	boolean sceneInvalidated = true;

	// Statistics
	long drawTime = 1000000000;
	long cumulativeDrawTime;
	long lastDrawTime;
	long maxDrawLatency;
	long latencyStartTime;
	long statMaxDrawLatency;
	int drawCount;
	boolean showStatistics = false;
	

	/**
	 * Creates an empty ScenicCanvas.
	 */
	public ScenicCanvas() {
		super();
		addComponentListener(new ResizeListener());
	}

	/**
	 * Sets the background color of the canvas.
	 */
	public void setBackground(Color color) {
		bgColor = color;
		change();
	}
	
	/**
	 * Gets whatever statistics are shown or not.
	 */
	public boolean isShowStatistics() {
		return showStatistics;
	}

	/**
	 * Sets whatever statistics are shown or not.
	 */
	public void setShowStatistics(boolean showStatistics) {
		this.showStatistics = showStatistics;
		change();
	}

	/**
	 * Sets the scene.
	 */
	public void setScene(SceneNode scene) {
		if(this.scene != null) {
			this.scene.removeVisibleParent(this);
		}
		this.scene = scene;
		if(this.scene != null) {
			this.scene.addVisibleParent(this);
		}
		change();
	}

	private void change() {
		if(!sceneChanged) {
			sceneChanged = true;
			repaint();
		}
	}
	
	/**
	 * Gets the scene.
	 */
	public SceneNode getScene() {
		return scene;
	}
	
	/**
	 * Paints the component. This method calls the updateScene() method 
	 * before rendering the scene if the scene has been invalidated.
	 */
	public void paint(Graphics g) {
		super.paint(g);
		if(sceneInvalidated) {
			sceneChanged = true;
			sceneInvalidated = false;
			updateScene();
		}
		sceneChanged = false;
		render();
	}
	
	/**
	 * Invalidates the scene. This method marks the scene as invalidated
	 * and calls the repaint() to ask AWT to paint the component. When the
	 * component is painted the paint() method calls the updateScene() method.
	 */
	public void invalidateScene() {
		if(!sceneInvalidated) {
			sceneInvalidated = true;
			repaint();
		}
	}
	
	private void updateStatistics(long startTime, long stopTime) {
		cumulativeDrawTime += stopTime - startTime;
		drawCount++;
		if(cumulativeDrawTime > 100 * 1000000) {
			drawTime = cumulativeDrawTime / drawCount;
			cumulativeDrawTime = 0;
			drawCount = 0;
		} 
		
		maxDrawLatency = Math.max(maxDrawLatency, stopTime - lastDrawTime);
		lastDrawTime = stopTime;
		
		if(stopTime - latencyStartTime > 1000 * 1000 * 1000) {
			statMaxDrawLatency = maxDrawLatency;
			maxDrawLatency = 0;
			latencyStartTime = stopTime;
		}		
	}
	
	private void render()
	{		
		Rectangle r = getBounds();		
		DrawContext context = new DrawContext();
		Rectangle bounds = new Rectangle(0, 0, r.width, r.height);
		long startTime = System.nanoTime();
		
		context.root = this;
		context.renderingInstance = new Object();
		context.width = r.width;
		context.height = r.height;
		context.renderer = new Renderer();
		context.context = context.renderer.createContextForCanvas(this);
		
		context.renderer.enterCriticalSection();
		try {
			if(!context.renderer.beginScene(context.context)) {
				System.out.println("beginScene failed");
				return;
			}
		

			context.renderer.color(context.context, bgColor);
			context.renderer.clear(context.context);
			
			if(scene != null) {
				scene.prepareDraw(context, Affine2.identity());
				scene.draw(context, Affine2.identity(), bounds);
			}

//			drawTime = Math.min(drawTime, stopTime - startTime);
			
			if(showStatistics)
				drawStatistics(context);
			
			context.renderer.endScene(context.context);
			context.renderer.present(context.context);
		} finally {
			context.renderer.deleteContext(context.context);
			context.renderer.leaveCriticalSection();			
		}
		
		long stopTime = System.nanoTime();
		
		updateStatistics(startTime, stopTime);
	}

	/**
	 * Updates the scene. This method should be overriden by classes
	 * that extends this class. This method should be responsible for
	 * updating and modifying the scene.
	 */
	public void updateScene() {		
	}
	
	private void drawStatistics(DrawContext context) {
		SceneContainer statScene = new SceneContainer();
		scenic.Graphics g = new scenic.Graphics(statScene, Affine2.identity());
		Rectangle r = getBounds();		
		Rectangle bounds = new Rectangle(0, 0, r.width, r.height);
		
		g.setFillColor(new Color(0.5f, 0.5f, 0.5f, 0.8f));
		g.rectangle(0, 0, 250, 50);
		g.fill();
		g.setFillColor(new Color(1.0f, 0.0f, 0.0f, 1.0f));
		g.setFont(new Font("SansSerif", Font.PLAIN, 22));
		g.translate(2, 20);
		g.drawText("Draw time: " + drawTime / 100000 / 10.0 + "ms");
		g.translate(2, 20);
		g.drawText("Max latency: " + statMaxDrawLatency / 100000 / 10.0 + "ms");

		statScene.prepareDraw(context, Affine2.identity());		
		statScene.draw(context, Affine2.identity(), bounds);		
	}

	/**
	 * This method is automatically called when the scene graph is changed.
	 * Do not call this method yourself. The scene is automatically
	 * repainted whenever it changes.
	 */
	public void childChanged(Rectangle area) {
		change();
	}

	private Affine2 getTotalTransform() {
		return Affine2.identity();
	}
}
